package com.liaoyifan.core.opera;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.liaoyifan.core.constant.Constants;
import com.liaoyifan.core.constant.Properties;
import com.liaoyifan.core.enums.CoreCodeEnum;
import com.liaoyifan.core.exception.AuthException;
import com.liaoyifan.core.exception.CoreException;
import com.liaoyifan.core.exception.NotFoundException;
import com.liaoyifan.core.model.ParamError;
import com.liaoyifan.core.model.Result;
import com.liaoyifan.core.util.Json;
import com.liaoyifan.core.util.Try;
import com.liaoyifan.core.valid.Message;
import feign.FeignException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import java.io.FileNotFoundException;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Arrays;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

@SuppressWarnings("unused")
@ControllerAdvice
@ResponseBody
@ConditionalOnProperty(
        name = Properties.GLOBAL_EXCEPTION_HANDLER_ENABLE,
        havingValue = Constants.TRUE)
public class GlobalExceptionHandler {

    public static final Logger log =
            LogManager.getLogger(GlobalExceptionHandler.class.getSimpleName());

    @ExceptionHandler(Throwable.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<Void> handleThrowable(Throwable e) {
        String message = Message.get(CoreCodeEnum.UNKNOWN.getMessage());
        log.error(message, e);
        return Result.error(message);
    }

    @ExceptionHandler(CoreException.class)
    public ResponseEntity<Result<?>> handleBaseException(CoreException e) {
        if (e instanceof AuthException) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Result.error(e.getCode(), Try.log(Message.get(e.getMessage()), e)));
        }
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(Result.error(e.getCode(), Try.log(Message.get(e.getMessage()), e)));
    }

    @ExceptionHandler(UndeclaredThrowableException.class)
    public ResponseEntity<Result<?>> handleUndeclaredThrowableException(
            UndeclaredThrowableException e, HttpServletRequest request) {
        if (e.getCause() instanceof BlockException cause) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                    .body(SentinelExceptionHandler.result(request, null, cause));
        }
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(handleThrowable(e));
    }

    @ExceptionHandler(FeignException.class)
    public ResponseEntity<?> handleHttpException(FeignException e) {
        Try.log(e.getMessage(), e);
        String content = e.contentUTF8();
        try {
            return ResponseEntity.status(e.status()).body(Json.parse(content, Result.class));
        } catch (Exception x) {
            return ResponseEntity.status(e.status()).body(Result.error(content));
        }
    }

    @ExceptionHandler({
        NotFoundException.class,
        NoResourceFoundException.class,
        NoHandlerFoundException.class,
        FileNotFoundException.class
    })
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Result<Void> handleNotFoundException(Exception e) {
        return Result.error(
                CoreCodeEnum.RESOURCE_NOT_FOUND.getCode(),
                Try.log(Message.get(CoreCodeEnum.RESOURCE_NOT_FOUND.getMessage()), e));
    }

    @ExceptionHandler({
        MethodArgumentTypeMismatchException.class,
        HttpRequestMethodNotSupportedException.class
    })
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public Result<Void> handleMethodNotAllowedException(Exception e) {
        return Result.error(
                CoreCodeEnum.METHOD_NOT_ALLOWED.getCode(),
                Try.log(Message.get(CoreCodeEnum.METHOD_NOT_ALLOWED.getMessage()), e));
    }

    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<List<ParamError>> handleBindException(BindException e) {
        List<ParamError> errors =
                e.getBindingResult().getFieldErrors().stream()
                        .map(
                                error ->
                                        new ParamError(
                                                error.getField(),
                                                error.getDefaultMessage(),
                                                String.valueOf(error.getRejectedValue())))
                        .toList();
        return Result.error(
                CoreCodeEnum.PARAM_ERROR.getCode(),
                Try.log(Message.get(CoreCodeEnum.PARAM_ERROR.getMessage()), e, errors),
                errors);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<List<ParamError>> handleConstraintViolationException(
            ConstraintViolationException e) {
        List<ParamError> errors =
                e.getConstraintViolations().stream()
                        .map(
                                error ->
                                        new ParamError(
                                                Arrays.stream(
                                                                error.getPropertyPath()
                                                                        .toString()
                                                                        .split("\\."))
                                                        .reduce((first, second) -> second)
                                                        .orElse("unknown"),
                                                error.getMessage(),
                                                String.valueOf(error.getInvalidValue())))
                        .toList();
        return Result.error(
                CoreCodeEnum.PARAM_ERROR.getCode(),
                Try.log(Message.get(CoreCodeEnum.PARAM_ERROR.getMessage()), e, errors),
                errors);
    }
}
